/*
 * ============================================================================
 *
 *  Zombie:Reloaded
 *
 *  File:          restrict.inc
 *  Type:          Core
 *  Description:   Weapon restriction system.
 *
 *  Copyright (C) 2009  Greyscale, Richard Helgeby
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * ============================================================================
 */

/**
 * Maximum types allowed for a single weapon.
 */
#define WEAPONS_RESTRICT_MAX_TYPES 8

/**
 * Restrict config data indexes.
 */
enum RestrictData
{
    RESTRICT_DATA_NAME = 0,
}

/**
 * Array that stores the "HookID" to be later unhooked on player disconnect.
 */
new g_iCanUseHookID[MAXPLAYERS + 1] = {-1, ...};

/**
 * Array to block any client from picking up weapons.
 */
new bool:g_bRestrictBlockWeapon[MAXPLAYERS + 1];

/**
 * Array to store a list of different weapon groups
 */
new Handle:arrayWeaponTypes = INVALID_HANDLE;

/**
 * Query results returned when (un)restricting a weapon.
 */
enum RestrictQuery
{
    Query_Successful,   /** (Un)restrict was successful. */
    Query_Stopped,      /** (Un)restrict was stopped because action was redundant. */
    Query_Locked,       /** (Un)restrict was stopped because the weapon is marked "untoggleable." */
    Query_Invalid,      /** (Un)restrict failed because invalid info was given. */
}

/**
 * Initialize data and hook commands.
 */
RestrictInit()
{
    // Hook buy command.
    RegConsoleCmd("buy", RestrictBuyCommand);
    RegConsoleCmd("autobuy", RestrictBuyCommand);
    RegConsoleCmd("rebuy", RestrictBuyCommand);
}

/**
 * Auto-create a list of types loaded by weapons module.
 */ 
RestrictLoad()
{
    // If array exists, then destroy it.
    if (arrayWeaponTypes != INVALID_HANDLE)
    {
        CloseHandle(arrayWeaponTypes);
    }
    
    // Initialize array.
    arrayWeaponTypes = CreateArray(WEAPONS_MAX_LENGTH);
    
    decl String:weapontype[WEAPONS_MAX_LENGTH];
    new String:weapontypes[WEAPONS_RESTRICT_MAX_TYPES][WEAPONS_MAX_LENGTH];
    
    // x = Array index.
    new size = GetArraySize(arrayWeapons);
    for (new x = 0; x < size; x++)
    {
        WeaponsGetType(x, weapontype, sizeof(weapontype));
        
        ExplodeString(weapontype, ",", weapontypes, sizeof(weapontypes), sizeof(weapontypes[]));
        for (new y = 0; y < WEAPONS_RESTRICT_MAX_TYPES; y++)
        {
            // Cut off whitespace.
            TrimString(weapontypes[y]);
            
            // If we've reached the end of the weapon's types, then stop.
            if (!weapontypes[y][0])
            {
                break;
            }
            
            // If the weapon type isn't in the main array, then push it in.
            if (FindStringInArray(arrayWeaponTypes, weapontypes[y]) == -1)
            {
                // Push weapon type name into weapon type array.
                PushArrayString(arrayWeaponTypes, weapontypes[y]);
            }
        }
    }
}

/**
 * Hook commands related to restrict here.
 */
RestrictOnCommandsCreate()
{
    // Create weapon admin commands.
    RegConsoleCmd("zr_restrict", RestrictCommand, "Restricts a weapon or a weapon type. Usage: zr_restrict <weapon|weapon type> [weapon2|weapontype2] ...");
    RegConsoleCmd("zr_unrestrict", UnrestrictCommand, "Unrestricts a weapon or a weapon type. Usage: zr_unrestrict <weapon|weapon type> [weapon2|weapontype2] ...");
}

/**
 * Client is joining the server.
 * 
 * @param client    The client index.
 */
RestrictClientInit(client)
{
    // Hook "Weapon_CanUse" on client.
    #if defined USE_SDKHOOKS
        SDKHook(client, SDKHook_WeaponCanUse, RestrictCanUse);
        
        // Set dummy value so it think it's hooked.
        g_iCanUseHookID[client] = 1;
    #else
        g_iCanUseHookID[client] = ZRTools_HookWeapon_CanUse(client, RestrictCanUse);
    #endif
    
    // Reset block weapons flag.
    g_bRestrictBlockWeapon[client] = false;
}

/**
 * Unhook Weapon_CanUse function on a client.
 * 
 * @param client    The client index.
 */
RestrictOnClientDisconnect(client)
{
    // Unhook "Weapon_CanUse" callback, and reset variable.
    if (g_iCanUseHookID[client] != -1)
    {
        #if defined USE_SDKHOOKS
            SDKUnhook(client, SDKHook_WeaponCanUse, RestrictCanUse);
        #else
            ZRTools_UnhookWeapon_CanUse(g_iCanUseHookID[client]);
        #endif
        
        g_iCanUseHookID[client] = -1;
    }
}

/**
 * Client is spawning into the game.
 * 
 * @param client    The client index.
 */
RestrictOnClientSpawn(client)
{
    // Re-hook "canuse" on client.
    #if defined USE_SDKHOOKS
        SDKUnhook(client, SDKHook_WeaponCanUse, RestrictCanUse);     // <--- What happens if it's not already hooked???
        SDKHook(client, SDKHook_WeaponCanUse, RestrictCanUse);
        g_iCanUseHookID[client] = 1;
    #else
        ZRTools_UnhookWeapon_CanUse(g_iCanUseHookID[client]);
        g_iCanUseHookID[client] = ZRTools_HookWeapon_CanUse(client, RestrictCanUse);
    #endif
    
    // H4x.  Unfortunately I can't disable this flag early enough for CS:S to give start USP/Glock.
    // So I have to give BOOTLEG weapons.  (Sorry Valve)
    if (g_bRestrictBlockWeapon[client])
    {
        // Reset block weapons flag.
        g_bRestrictBlockWeapon[client] = false;
        
        if (ZRIsClientOnTeam(client, CS_TEAM_T))
        {
            GivePlayerItem(client, WEAPONS_SPAWN_T_WEAPON);
        }
        else if (ZRIsClientOnTeam(client, CS_TEAM_CT))
        {
            GivePlayerItem(client, WEAPONS_SPAWN_CT_WEAPON);
        }
    }
}

/**
 * The round is ending.
 */
RestrictOnRoundEnd()
{
    new bool:restrictzombieequip = GetConVarBool(g_hCvarsList[CVAR_WEAPONS_RESTRICT_ENDEQUIP]);
    if (!restrictzombieequip)
    {
        return;
    }
    
    // x = Client index.
    for (new x = 1; x <= MaxClients; x++)
    {
        // If client isn't in-game, then stop.
        if (!IsClientInGame(x))
        {
            continue;
        }
        
        // If client is a human, then stop.
        if (InfectIsClientHuman(x))
        {
            continue;
        }
        
        // Enable block weapon flag.
        g_bRestrictBlockWeapon[x] = true;
    }
}

/**
 * Command callback function for the "buy" command
 * Used to block use of this command under certain conditions.
 * 
 * @param client    The client index.
 * @param argc      Argument count.
 */
public Action:RestrictBuyCommand(client, argc)
{
    // If client isn't valid, then stop.
    if (!ZRIsClientValid(client) || !IsClientInGame(client))
    {
        return Plugin_Continue;
    }
    
    // If the client isn't in a buyzone, then stop.
    if (!WeaponsIsClientInBuyZone(client))
    {
        return Plugin_Continue;
    }
    
    // If player is a zombie, then block command.
    if (InfectIsClientInfected(client))
    {
        TranslationPrintToChat(client, "Zombie cant use weapon");
        
        // Block command
        return Plugin_Handled;
    }
    
    decl String:weapon[WEAPONS_MAX_LENGTH];
    GetCmdArg(1, weapon, sizeof(weapon));
    
    // If the weapon is restricted, then prevent pickup.
    decl String:weaponname[WEAPONS_MAX_LENGTH];
    new weaponindex = WeaponsEntityToDisplay(weapon, weaponname, sizeof(weaponname), true);
    
    // If weapon isn't configged, then allow pickup.
    if (weaponindex == -1)
    {
        // Allow command.
        return Plugin_Continue;
    }
    
    // If weapon is restricted, then stop.
    if (RestrictIsWeaponRestricted(weaponindex))
    {
        TranslationPrintToChat(client, "Weapon is restricted", weaponname);
        
        // Block command.
        return Plugin_Handled;
    }
    
    // Allow command.
    return Plugin_Continue;
}

/**
 * Restricts (or unrestricts) a given weapon or weapon type.
 * 
 * @param restrict      True to restrict, false to unrestrict.
 * @param target        Weapon or weapon type to restrict/unrestrict.
 * @param single        True if a single weapon is being restricted, false if weapon type.
 * @param returntarget  The proper targetname. (same as param 'target' if invalid)
 * @param maxlen        The maximum length of param 'returntarget.'
 * @return              Query result. (See enum RestrictQuery)
 */
RestrictQuery:RestrictWeapon(bool:restrict, const String:target[], &bool:single = true, String:returntarget[], maxlen)
{
    // Copy 'target' to 'returntarget' to be possibly changed later.
    strcopy(returntarget, maxlen, target);
    
    // Find index of the given weapon type.
    new typeindex = RestrictTypeToIndex(returntarget);
    
    // Single weapon.
    if (typeindex == -1)
    {
        single = true;
        
        new weaponindex = WeaponsNameToIndex(returntarget);
        
        // If weapon index is invalid, then return invalid.
        if (weaponindex == -1)
        {
            return Query_Invalid;
        }
        
        // Return proper weapon name.
        WeaponsGetName(weaponindex, returntarget, maxlen);
        
        // If weapon is untoggleable, then return locked.
        if (!WeaponsGetToggleable(weaponindex))
        {
            return Query_Locked;
        }
        
        // If weapon restriction is redundant then return stopped.
        if (RestrictIsWeaponRestricted(weaponindex) == restrict)
        {
            return Query_Stopped;
        }
        
        // Set weapon's restricted state.
        RestrictSetWeaponRestricted(weaponindex, false, restrict);
        
        // Successfully restricted weapon.
        return Query_Successful;
    }
    // Weapon type.
    else
    {
        single = false;
        
        // Get all weapons in the given type.
        new Handle:arrayTypeWeapons;
        new count = RestrictGetTypeWeapons(typeindex, arrayTypeWeapons);
        
        // Return proper weapon name.
        RestrictWeaponTypeGetName(typeindex, returntarget, maxlen);
        
        // If weapon restriction is redundant then return stopped.
        if (RestrictIsTypeUniform(restrict, typeindex))
        {
            return Query_Stopped;
        }
        
        for (new x = 0; x < count; x++)
        {
            // Get weapon index.
            new weaponindex = GetArrayCell(arrayTypeWeapons, x);
            
            // If weapon is untoggleable, then stop.
            if (!WeaponsGetToggleable(weaponindex))
            {
                continue;
            }
            
            // Set weapon's restricted state.
            RestrictSetWeaponRestricted(weaponindex, false, restrict);
        }
        
        // Successfully restricted weapon type.
        return Query_Successful;
    }
}

/** Print weapon (un)restriction query result to client(s).
 *
 * @param client    The client to print response to. (0 for all clients)
 * @param query     The query result.
 * @param single    True if a single weapon is being restricted.
 * @param restrict  True if the query was to restrict/unrestrict a weapon.
 * @param target    The target to be restricted/unrestricted.
 */
RestrictPrintQueryResponse(client, RestrictQuery:query, bool:single, bool:restrict, const String:target[])
{
    switch(query)
    {
        // Query was successful.
        case Query_Successful:
        {
            if (single)
            {
                if (restrict)
                {
                    TranslationPrintToChatAll(true, false, "Restrict weapon", target);
                }
                else
                {
                    TranslationPrintToChatAll(true, false, "Unrestrict weapon", target);
                }
            }
            else
            {
                if (restrict)
                {
                    TranslationPrintToChatAll(true, false, "Restrict weapon type", target);
                }
                else
                {
                    TranslationPrintToChatAll(true, false, "Unrestrict weapon type", target);
                }
            }
        }
        // Query was redundant.
        case Query_Stopped:
        {
            if (single)
            {
                if (restrict)
                {
                    TranslationReplyToCommand(client, "Restrict weapon stopped", target);
                }
                else
                {
                    TranslationReplyToCommand(client, "Unrestrict weapon stopped", target);
                }
            }
            else
            {
                if (restrict)
                {
                    TranslationReplyToCommand(client, "Restrict weapon type stopped", target);
                }
                else
                {
                    TranslationReplyToCommand(client, "Unrestrict weapon type stopped", target);
                }
            }
        }
        // Weapon is untoggleable.
        case Query_Locked:
        {
            TranslationReplyToCommand(client, "Restrict weapon untoggleable", target);
        }
        // Weapon was invalid.
        case Query_Invalid:
        {
            TranslationReplyToCommand(client, "Weapon invalid", target);
        }
    }
}

stock RestrictTypeToIndex(const String:type[])
{
    decl String:typename[WEAPONS_MAX_LENGTH];
    
    // x = Array index.
    new size = GetArraySize(arrayWeaponTypes);
    for (new x = 0; x < size; x++)
    {
        RestrictWeaponTypeGetName(x, typename, sizeof(typename));
        
        // If types match, then return index.
        if (StrEqual(type, typename, false))
        {
            return x;
        }
    }
    
    // Type doesn't exist.
    return -1;
}

/**
 * Gets the name of a weapon type at a given index.
 * @param index     The weapon type index.
 * @param weapon    The string to return name in.
 * @param maxlen    The max length of the string.
 */
stock RestrictWeaponTypeGetName(index, String:weapontype[], maxlen)
{
    // Get weapon type name at given index.
    GetArrayString(arrayWeaponTypes, index, weapontype, maxlen);
}

/**
 * Returns an array containing all weapon indexes matching the given type.
 * 
 * @param index             The weapon type index.
 * @param arrayTypeWeapons  A handle to store array containing matching weapons.
 *                          Don't forget to close this!
 */
stock RestrictGetTypeWeapons(index, &Handle:arrayTypeWeapons)
{
    // Create array to hold weapons of the given type.
    arrayTypeWeapons = CreateArray();
    
    // Get name of the weapon type at given index.
    decl String:typename[WEAPONS_MAX_LENGTH];
    RestrictWeaponTypeGetName(index, typename, sizeof(typename));
    
    new count;
    decl String:weapontype[WEAPONS_MAX_LENGTH];
    new String:weapontypes[WEAPONS_RESTRICT_MAX_TYPES][WEAPONS_MAX_LENGTH];
    
    // x = Array index.
    new size = GetArraySize(arrayWeapons);
    for (new x = 0; x < size; x++)
    {
        WeaponsGetType(x, weapontype, sizeof(weapontype));
        
        ExplodeString(weapontype, ",", weapontypes, sizeof(weapontypes), sizeof(weapontypes[]));
        for (new y = 0; y < WEAPONS_RESTRICT_MAX_TYPES; y++)
        {
            // Cut off whitespace.
            TrimString(weapontypes[y]);
            
            // If we've reached the end of the weapon's types, then stop.
            if (!weapontypes[y][0])
            {
                break;
            }
            
            // If types match, then add weapon to array.
            if (StrEqual(typename, weapontypes[y], false))
            {
                PushArrayCell(arrayTypeWeapons, x);
                count++;
            }
        }
    }
    
    // Return number of weapons of the given type.
    return count;
}

/**
 * Gets the restricted status on a weapon.
 * 
 * @param index     The weapon index.
 * @param toggle    If true, the value is toggled, otherwise 'restrict' param is used.
 * @param restrict  (Only if 'toggle' is 'false') Restricted status of the weapon.
 */
stock RestrictSetWeaponRestricted(index, bool:toggle, bool:restrict = false)
{
    // Get array handle of weapon at given index.
    new Handle:arrayWeapon = GetArrayCell(arrayWeapons, index);
    
    // Set restricted status.
    new bool:value = toggle ? !RestrictIsWeaponRestricted(index) : restrict;
    SetArrayCell(arrayWeapon, _:WEAPONS_DATA_RESTRICTED, value);
}

/**
 * Gets the restricted status on a weapon.
 * 
 * @param index     The weapon index.
 * @return          True if weapon is restricted, false if not.
 */
stock bool:RestrictIsWeaponRestricted(index)
{
    // Get array handle of weapon at given index.
    new Handle:arrayWeapon = GetArrayCell(arrayWeapons, index);
    
    // Return restricted status.
    return bool:GetArrayCell(arrayWeapon, _:WEAPONS_DATA_RESTRICTED);
}

/**
 * Used to check if all weapons of a type are restricted.
 * 
 * @param restricted    True to check if all weapons of given type are restricted.
 * @param index         The weapon type index.
 * @return              True if all weapons of the given type are restricted or not, false if not.
 */
stock bool:RestrictIsTypeUniform(bool:restricted, index)
{
    new Handle:arrayTypeWeapons;
    new count = RestrictGetTypeWeapons(index, arrayTypeWeapons);
    
    // x = array index
    for (new x = 0; x < count; x++)
    {
        // Get weapon index to check restricted status of.
        new weaponindex = GetArrayCell(arrayTypeWeapons, x);
        
        // If weapon is toggleable and it's not uniform with the given status, then return false.
        if (WeaponsGetToggleable(weaponindex) && RestrictIsWeaponRestricted(weaponindex) != restricted)
        {
            return false;
        }
    }
    
    // All weapons are restricted, so return true.
    return true;
}

/**
 * Hook callback, called when a player is trying to pick up a weapon.
 * @param client    The client index.
 * @param weapon    The weapon index.
 * @return          Return ZRTools_Handled to stop weapon pickup.
 *                  ZRTools_Continue to allow weapon pickup.
 */
#if defined USE_SDKHOOKS
public Action:RestrictCanUse(client, weapon)
#else
public ZRTools_Action:RestrictCanUse(client, weapon)
#endif
{
    new String:weaponentity[WEAPONS_MAX_LENGTH];
    GetEdictClassname(weapon, weaponentity, sizeof(weaponentity));
    
    // If weapon is a knife, then allow pickup.
    if (StrEqual(weaponentity, "weapon_knife"))
    {
        return ACTION_CONTINUE;
    }
    
    // If the player is a zombie, then prevent pickup.
    if (InfectIsClientInfected(client))
    {
        return ACTION_HANDLED;
    }
    
    // If client is flagged for not picking up weapons, then stop.
    if (g_bRestrictBlockWeapon[client])
    {
        return ACTION_HANDLED;
    }
    
    // If weapons module is disabled, then stop.
    new bool:weapons = GetConVarBool(g_hCvarsList[CVAR_WEAPONS]);
    if (!weapons)
    {
        return ACTION_CONTINUE;
    }
    
    // If restrict module is disabled, then stop.
    new bool:restrict = GetConVarBool(g_hCvarsList[CVAR_WEAPONS_RESTRICT]);
    if (!restrict)
    {
        return ACTION_CONTINUE;
    }
    
    // If the weapon is restricted, then prevent pickup.
    decl String:weaponname[WEAPONS_MAX_LENGTH];
    new weaponindex = WeaponsEntityToDisplay(weaponentity, weaponname, sizeof(weaponname));
    
    // If weapon isn't configged, then allow pickup.
    if (weaponindex == -1)
    {
        // Allow pickup.
        return ACTION_CONTINUE;
    }
    
    // If weapon is restricted, then stop.
    if (RestrictIsWeaponRestricted(weaponindex))
    {
        return ACTION_HANDLED;
    }
    
    // Forward event to weapons module.
    WeaponsOnItemPickup(client, weapon);
    
    // Allow pickup.
    return ACTION_CONTINUE;
}

/**
 * Command callbacks.
 */

/**
 * Command callback (zr_restrict)
 * Restricts a weapon or group
 *   
 * @param client    The client index.
 * @param argc      Argument count.
 */
public Action:RestrictCommand(client, argc)
{
    // Check if privileged.
    if (!ZRIsClientPrivileged(client, OperationType_Configuration))
    {
        TranslationReplyToCommand(client, "No access to command");
        return Plugin_Handled;
    }
    
    // If weapons module is disabled, then stop.
    new bool:weapons = GetConVarBool(g_hCvarsList[CVAR_WEAPONS]);
    if (!weapons)
    {
        // Tell client command is disabled.
        TranslationReplyToCommand(client, "Feature is disabled");
        return Plugin_Handled;
    }
    
    // If restrict module is disabled, then stop.
    new bool:restrict = GetConVarBool(g_hCvarsList[CVAR_WEAPONS_RESTRICT]);
    if (!restrict)
    {
        // Tell client command is disabled.
        TranslationReplyToCommand(client, "Feature is disabled");
        return Plugin_Handled;
    }
    
    // If not enough arguments given, then stop.
    if (argc < 1)
    {
        TranslationReplyToCommand(client, "Weapons command restrict syntax");
        return Plugin_Handled;
    }
    
    decl String:target[WEAPONS_MAX_LENGTH];
    
    new args = GetCmdArgs();
    for (new x = 1; x <= args; x++)
    {
        // Get target to restrict.
        GetCmdArg(x, target, sizeof(target));
        
        // Query restrict on this target, and get a result back.
        new bool:single;
        decl String:returntarget[WEAPONS_MAX_LENGTH];
        new RestrictQuery:query = RestrictWeapon(true, target, single, returntarget, sizeof(returntarget));
        
        // Print response to client(s).
        RestrictPrintQueryResponse(client, query, single, true, returntarget);
    }
    
    return Plugin_Handled;
}

/**
 * Command callback (zr_unrestrict)
 * Unrestricts a weapon or group
 *   
 * @param client    The client index.
 * @param argc      Argument count.
 */
public Action:UnrestrictCommand(client, argc)
{
    // Check if privileged.
    if (!ZRIsClientPrivileged(client, OperationType_Configuration))
    {
        TranslationReplyToCommand(client, "No access to command");
        return Plugin_Handled;
    }
    
    // If weapons module is disabled, then stop.
    new bool:weapons = GetConVarBool(g_hCvarsList[CVAR_WEAPONS]);
    if (!weapons)
    {
        // Tell client command is disabled.
        TranslationReplyToCommand(client, "Feature is disabled");
        return Plugin_Handled;
    }
    
    // If restrict module is disabled, then stop.
    new bool:restrict = GetConVarBool(g_hCvarsList[CVAR_WEAPONS_RESTRICT]);
    if (!restrict)
    {
        // Tell client command is disabled.
        TranslationReplyToCommand(client, "Feature is disabled");
        return Plugin_Handled;
    }
    
    // If not enough arguments given, then stop.
    if (argc < 1)
    {
        TranslationReplyToCommand(client, "Weapons command unrestrict syntax");
        return Plugin_Handled;
    }
    
    // arg1 = weapon being restricted
    decl String:target[WEAPONS_MAX_LENGTH];
    
    new args = GetCmdArgs();
    for (new x = 1; x <= args; x++)
    {
        // Get target to restrict.
        GetCmdArg(x, target, sizeof(target));
        
        // Query unrestrict on this target, and get a result back.
        new bool:single;
        decl String:returntarget[WEAPONS_MAX_LENGTH];
        new RestrictQuery:query = RestrictWeapon(false, target, single, returntarget, sizeof(returntarget));
        
        // Print response to client(s).
        RestrictPrintQueryResponse(client, query, single, false, returntarget);
    }
    
    return Plugin_Handled;
}
